#!/usr/bin/env perl
#
# Copyright (c) 2012-2013 Jeffrey M. Squyres.  All rights reserved.
#
# $COPYRIGHT$
# 
# Additional copyrights may follow
# 
# $HEADER$
#

package RubySlippers::Query::MDNS;

use strict;
use RubySlippers::Messages;
use RubySlippers::Util;
use threads;
use threads::shared;
use Socket;


# Output analysis filename
my $_analysis_filename;

# Interface(s) to listen on
my @_interfaces;

# Listening thread IDs
my @_tids;

# Timeout (in seconds) within which we see the same device again, we
# ignore it
my $_revisible_timeout = 15;

# Time to quit
my $_time_to_quit;

# Devices currently found (written by listening thread, read by the
# main thread)
my %_mdns_devices;
share(%_mdns_devices);


# This function runs continually in its own thread (but it blocks the
# vast majority of the time).
sub _mdns_thread_main {
    my $ipdevice = shift;

    Debug("starting tcpdump: $ipdevice");

    open(TCPDUMP, "tcpdump -l -i $ipdevice port mdns 2>&1|")
        || die "Can't open tcpdump on $ipdevice";
    while (<TCPDUMP>) {
        if (m/\(Cache flush\) SRV ([a-zA-Z0-9_\-]+?)\.local/) {
            # Found an MDNS entry.  Lock the hash and update its
            # timestamp.
            lock(%_mdns_devices);
            my $t = time;
            if (exists($_mdns_devices{$1})) {
                my $elapsed = $t - $_mdns_devices{$1}{"last_seen_timestamp"};
                # If we saw <N seconds ago, then skip this -- it was
                # part of the same MDNS update we just recently saw.
                next
                    if ($elapsed < $_revisible_timeout);

                Debug("Now: $t, old $1 (saw it $elapsed seconds ago)");
            } else {
                Debug("Now: $t, NEW $1");
                # If this is a new device, make a shared hash for it,
                # and make one of its members be a shared array
                $_mdns_devices{$1} = shared_clone({ 
                    device => $1,
                    all_timestamps => shared_clone([]),
                                              });
            }

            # Save this timestamp in the "last" field (for
            # convenience) and in the array of all timestamps that
            # we've seen it so far.  Shared data in perl can only
            # (easily) be one-leve hashes, so just suffix the hash
            # name with ":next_level_field_name" to emulate multiple
            # levels.
            $_mdns_devices{$1}{"last_seen_timestamp"} = $t;
            push(@{$_mdns_devices{$1}{"all_timestamps"}}, $t);
        }
    }
    close(TCPDUMP);

    Debug("============================ tcpdump finished on $ipdevice");
    threads->exit(0);
}


sub _mdns_query {
    # Make sure that there is a tcpdump thread running
    my $is_running;
    foreach my $tid (@_tids) {
        if ($tid->is_running()) {
            $is_running = 1;
            last;
        }
    }
    if (!$is_running) {
        Debug("No tcpdump thread running -- not doing anything");
        return undef;
    }

    # Lock the hash so that we don't access it at the same time as the
    # thread.  Prune any old devices.
    lock(%_mdns_devices);
    my $t = time;
    my $decision_args = {
        query => "mdns",
    };
    foreach my $d (sort(keys(%_mdns_devices))) {
        # For debugging and external analysis: show any new timestamps
        my ($t, $last_t);
        if (defined($_analysis_filename)) {
            open(OUT, ">>$_analysis_filename") || 
                die "Can't open $_analysis_filename for writing";
        }
        my $debug;
        while ($t = pop(@{$_mdns_devices{$d}{"all_timestamps"}})) {
            my $str = "DATA," . localtime . ",$t,$d";
            $str .= "," . ($t - $last_t)
                if (defined($last_t));

            print OUT localtime . ",$t,$d"
                if (defined($_analysis_filename));

            # For each device that has an all_timestamps entry (i.e.,
            # it was seen in this iteration), save it to pass on to
            # the Decide function.
            $decision_args->{devices}->{$d} = $t;

            # Make a string to show if we're debugging (this makes us
            # only show the *last* timestamp -- not all of them; just
            # to cut down on the amount of debug output shown).
            my $lt = localtime($t);
            $lt =~ s/^.+(\d\d:\d\d:\d\d).+/\1/;
            $debug = "MDNS: Saw $d at $lt";

            $last_t = $t;
        }
        Debug($debug)
            if (defined($debug));

        close(OUT)
            if (defined($_analysis_filename));
    }

    # If we have found any devices, make a Decision about it
    if (exists($decision_args->{devices})) {
        RubySlippers::Engine::Decide($decision_args);
    }
}


# Shut down at the end of the run
sub _mdns_close {
    foreach my $tid (@_tids) {
        #Debug("Joining listening thread...");
        # JMS This will block forever because we're likely in a
        # <TCPDUMP> read, and who knows when the next line will be
        # read?
        #$tid->join();
        #Debug("Joined");
    }
    @_tids = [];
}


# Find all tcpdump-capable devices
sub _mdns_find_interfaces {
    open(TCPDUMP, "tcpdump -D|") || die "Can't open tcpdump";
    while (<TCPDUMP>) {
        m/^\d+\.([a-zA-Z0-9_\-]+) *.*$/;
        # Is it loopback?
        if ($1 eq "lo" || $1 eq "lo0") {
            Debug("Found loopback IP interface ($1); skipping");
            next;
        }

        Debug("Found viable IP interface: $1");
        push(@_interfaces, $1);
    }
    close(TCPDUMP);
}


# Start a monitoring thread for every tcpdump device that is not
# localhost
sub _mdns_start_threads {
    if ($#_interfaces < 0) {
        _mdns_find_interfaces();
    }

    if ($#_interfaces < 0) {
        Warning("No viable IP interfaces; nothing to do");
        return undef;
    }

    # Ok, we found some viable IP devices; start a thread for each of
    # them
    foreach my $i (@_interfaces) {
        my $tid = threads->create(\&_mdns_thread_main, $i);
        if (!defined($tid)) {
            Warning("Failed to create tcpdump listening thread for $i");
            exit(1);
        }
        push(@_tids, $tid);
    }
    Verbose("MDNS returning Query module");

    # Create an array of 1 module to return
    my @ret;
    push(@ret, {
        frequency => 10,
        callback => \&_mdns_query,
        close => \&_mdns_close,
        name => "MDNS query (" . join(", ", @_interfaces) . ")",
         });
    return \@ret;
}

sub open {
    $_analysis_filename = RubySlippers::Config::get("mdns_analysis_filename");

    # Look for tcpdump anywhere in the path
    my $tcpdump = RubySlippers::Util::path_find("tcpdump");
    if (!defined($tcpdump)) {
        Verbose("MDNS: Did not find tcpdump; skipping");
        return undef;
    }

    Debug("MDNS found $tcpdump");
    return _mdns_start_threads();
}

1;
